import { ConnectionPool } from '@/core/connection-pool';
import { Producer } from '@/core/producer';
import { Consumer } from '@/core/consumer';
import { Message } from '@/core/message';
import { MessageHandler, Logger, MessageError, RPCMethod, RPCMethodRegistry } from '@/types';
import { createLogger } from '@/utils/logger';
import { generateUUID, generateCorrelationId } from '@/utils/uuid';

/**
 * RPC (Remote Procedure Call) pattern implementation
 */
export class RPC {
  private readonly connectionPool: ConnectionPool;
  private readonly serviceName: string;
  private readonly requestQueue: string;
  private readonly producer: Producer;
  private readonly logger: Logger;
  private readonly pendingCalls = new Map<
    string,
    {
      resolve: (value: unknown) => void;
      reject: (error: Error) => void;
      timeout: NodeJS.Timeout;
    }
  >();
  private replyConsumer?: Consumer;
  private replyQueueName?: string;
  private serverConsumer?: Consumer;

  constructor(connectionPool: ConnectionPool, serviceName: string, logger?: Logger) {
    this.connectionPool = connectionPool;
    this.serviceName = serviceName;
    this.requestQueue = `${serviceName}_rpc`;
    this.producer = new Producer(connectionPool);
    this.logger = logger ?? createLogger('RPC');
  }

  /**
   * Setup RPC client
   */
  async setupClient(): Promise<void> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      // Create exclusive reply queue
      const queueResult = await channel.assertQueue('', {
        exclusive: true,
        autoDelete: true,
      });

      this.replyQueueName = queueResult.queue;

      // Setup reply consumer
      this.replyConsumer = new Consumer(this.connectionPool, {
        handle: async (message: Message) => {
          const correlationId = message.getProperty('correlationId') as string;
          const pendingCall = this.pendingCalls.get(correlationId);

          if (pendingCall) {
            clearTimeout(pendingCall.timeout);
            this.pendingCalls.delete(correlationId);

            const result = message.body as { result?: unknown; error?: string };

            if (result.error) {
              pendingCall.reject(new Error(result.error));
            } else {
              pendingCall.resolve(result.result);
            }
          }

          return { success: true, shouldRetry: false };
        },
      });

      await this.replyConsumer.startConsuming({
        queueName: this.replyQueueName,
        autoAck: true,
      });

      this.logger.info(`RPC client setup completed for service ${this.serviceName}`);
    } catch (error) {
      this.logger.error(
        `Failed to setup RPC client for service ${this.serviceName}`,
        error as Error
      );
      throw error;
    }
  }

  /**
   * Make an RPC call
   */
  async call(
    methodName: string,
    params: Record<string, unknown> = {},
    timeoutMs = 30000
  ): Promise<unknown> {
    if (!this.replyQueueName) {
      await this.setupClient();
    }

    const correlationId = generateCorrelationId();
    const message = new Message({
      method: methodName,
      params,
    });

    message.addProperty('correlationId', correlationId);
    message.addProperty('replyTo', this.replyQueueName);

    return new Promise((resolve, reject) => {
      // Set timeout
      const timeout = setTimeout(() => {
        this.pendingCalls.delete(correlationId);
        reject(new Error(`RPC call timeout for method ${methodName}`));
      }, timeoutMs);

      // Store pending call
      this.pendingCalls.set(correlationId, { resolve, reject, timeout });

      // Send request
      this.producer
        .send(message, {
          routingKey: this.requestQueue,
          persistent: false,
        })
        .catch(error => {
          clearTimeout(timeout);
          this.pendingCalls.delete(correlationId);
          reject(error);
        });
    });
  }

  /**
   * Setup RPC server
   */
  async setupServer(
    methodRegistry: RPCMethodRegistry
  ): Promise<void> {
    try {
      const connection = await this.connectionPool.getConnection();
      const channel = connection.channel;

      // Declare request queue
      await channel.assertQueue(this.requestQueue, {
        durable: true,
      });

      // Setup server consumer
      this.serverConsumer = new Consumer(this.connectionPool, {
        handle: async (message: Message) => {
          const requestData = message.body as {
            method: string;
            params: Record<string, unknown>;
          };

          const correlationId = message.getProperty('correlationId') as string;
          const replyTo = message.getProperty('replyTo') as string;

          let response: { result?: unknown; error?: string };

          try {
            const method = methodRegistry[requestData.method];

            if (!method) {
              response = { error: `Method ${requestData.method} not found` };
            } else {
              const result = await method(requestData.params);
              response = { result };
            }
          } catch (error) {
            response = { error: (error as Error).message };
          }

          // Send reply
          if (replyTo && correlationId) {
            const replyMessage = new Message(response);
            replyMessage.addProperty('correlationId', correlationId);

            await this.producer.send(replyMessage, {
              routingKey: replyTo,
              persistent: false,
            });
          }

          return { success: true, shouldRetry: false };
        },
      });

      await this.serverConsumer.startConsuming({
        queueName: this.requestQueue,
        prefetchCount: 1,
        autoAck: false,
      });

      this.logger.info(`RPC server setup completed for service ${this.serviceName}`);
    } catch (error) {
      this.logger.error(
        `Failed to setup RPC server for service ${this.serviceName}`,
        error as Error
      );
      throw error;
    }
  }

  /**
   * Close RPC client
   */
  async closeClient(): Promise<void> {
    // Clear pending calls
    for (const [correlationId, pendingCall] of this.pendingCalls) {
      clearTimeout(pendingCall.timeout);
      pendingCall.reject(new Error('RPC client closing'));
    }
    this.pendingCalls.clear();

    if (this.replyConsumer) {
      await this.replyConsumer.close();
      this.replyConsumer = undefined;
    }

    this.replyQueueName = undefined;
    this.logger.info(`RPC client closed for service ${this.serviceName}`);
  }

  /**
   * Close RPC server
   */
  async closeServer(): Promise<void> {
    if (this.serverConsumer) {
      await this.serverConsumer.close();
      this.serverConsumer = undefined;
    }

    this.logger.info(`RPC server closed for service ${this.serviceName}`);
  }

  /**
   * Close both client and server
   */
  async close(): Promise<void> {
    await Promise.all([this.closeClient(), this.closeServer(), this.producer.close()]);

    this.logger.info(`RPC ${this.serviceName} closed`);
  }

  /**
   * Get RPC statistics
   */
  getStats(): {
    pendingCalls: number;
    serviceName: string;
    hasClient: boolean;
    hasServer: boolean;
  } {
    return {
      pendingCalls: this.pendingCalls.size,
      serviceName: this.serviceName,
      hasClient: !!this.replyConsumer,
      hasServer: !!this.serverConsumer,
    };
  }
}

/**
 * RPC Client for making remote calls
 */
export class RPCClient {
  private readonly rpc: RPC;

  constructor(connectionPool: ConnectionPool, serviceName: string, logger?: Logger) {
    this.rpc = new RPC(connectionPool, serviceName, logger);
  }

  async initialize(): Promise<void> {
    await this.rpc.setupClient();
  }

  async call(
    methodName: string,
    params?: Record<string, unknown>,
    timeoutMs?: number
  ): Promise<unknown> {
    return await this.rpc.call(methodName, params, timeoutMs);
  }

  async close(): Promise<void> {
    await this.rpc.closeClient();
  }

  getStats(): { pendingCalls: number; serviceName: string } {
    const stats = this.rpc.getStats();
    return {
      pendingCalls: stats.pendingCalls,
      serviceName: stats.serviceName,
    };
  }
}

/**
 * RPC Server for handling remote calls
 */
export class RPCServer {
  private readonly rpc: RPC;
  private readonly methodRegistry: RPCMethodRegistry = {};

  constructor(connectionPool: ConnectionPool, serviceName: string, logger?: Logger) {
    this.rpc = new RPC(connectionPool, serviceName, logger);
  }

  /**
   * Register a method
   */
  registerMethod(methodName: string, method: RPCMethod): void {
    this.methodRegistry[methodName] = method;
  }

  /**
   * Register multiple methods
   */
  registerMethods(methods: RPCMethodRegistry): void {
    Object.assign(this.methodRegistry, methods);
  }

  /**
   * Start the server
   */
  async start(): Promise<void> {
    await this.rpc.setupServer(this.methodRegistry);
  }

  /**
   * Stop the server
   */
  async stop(): Promise<void> {
    await this.rpc.closeServer();
  }

  /**
   * Get registered methods
   */
  getRegisteredMethods(): string[] {
    return Object.keys(this.methodRegistry);
  }
}

/**
 * Utility functions for creating RPC instances
 */
export const createRPCClient = (
  connectionPool: ConnectionPool,
  serviceName: string,
  logger?: Logger
): RPCClient => {
  return new RPCClient(connectionPool, serviceName, logger);
};

export const createRPCServer = (
  connectionPool: ConnectionPool,
  serviceName: string,
  logger?: Logger
): RPCServer => {
  return new RPCServer(connectionPool, serviceName, logger);
};

export const createRPC = (
  connectionPool: ConnectionPool,
  serviceName: string,
  logger?: Logger
): RPC => {
  return new RPC(connectionPool, serviceName, logger);
};
